﻿//-----------------------------------------------------------------------
// <copyright file="SelectionTool.cs" company="Nathan Miller">
// Copyright (c) Nathan Miller.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
//-----------------------------------------------------------------------
namespace TileTool.Base.Tool.Selection
{
    using System;
    using System.Diagnostics;
    using TileTool.Action.Message;
    using TileTool.Base.Editor.Tool.Scene;
    using TileTool.Common.Math;
    using TileTool.Editor.Tool.Scene;
    using TileTool.Editor.Tool.Selection;
    using TileTool.Input;
    using TileTool.Input.Focus;
    using TileTool.Input.Mouse;
    using TileTool.Render;
    using TileTool.Scene;
    using TileTool.Scene.Action;
    using TileTool.Scene.Collision;
    using TileTool.Scene.Entity;
    using TileTool.Settings;
    using TileTool.Tool;

    /// <summary>
    /// Base class for camera tools.
    /// </summary>
    public class SelectionTool : SceneToolBase
    {
        #region Constants - Private

        /// <summary>
        /// Width for the selection rectangle.
        /// </summary>
        private const float SelectionRectWidth = 1.0f;

        /// <summary>
        /// Time window for click selection cycling to be enabled.
        /// </summary>
        private const int SelectionCycleTimeMS = 1000;

        #endregion

        #region Fields - Private

        /// <summary>
        /// True if the mouse is down for selection.
        /// </summary>
        private bool mouseDown = false;

        /// <summary>
        /// Position of the mouse when it was pressed.
        /// </summary>
        private Vector2D mouseDownPos = Vector2D.Zero;

        /// <summary>
        /// Position of the mouse currently. Only valid if mouseDown is true.
        /// </summary>
        private Vector2D mouseCurrentPos = Vector2D.Zero;

        /// <summary>
        /// Settings for the selection tool.
        /// </summary>
        private SelectionToolSettings settings = null;

        /// <summary>
        /// Time of the last mouse click.
        /// </summary>
        private DateTime? lastClickTime = null;

        #endregion

        #region Constructor

        /// <summary>
        /// Default constructor.
        /// </summary>
        public SelectionTool()
        {
            this.Mode = ToolMode.Permanent;
            this.settings = SettingsSystem.Get<SelectionToolSettings>();
        }

        #endregion

        #region Methods - Public

        /// <summary>
        /// Draw the tool.
        /// </summary>
        /// <param name="renderData">Rendering data. Always non-null.</param>
        public override void Draw(IRenderData renderData)
        {
            base.Draw(renderData);

            if (!this.mouseDown)
            {
                return;
            }

            if (!this.HasValidBoxSelectionRect())
            {
                return;
            }

            Vector2D min = new Vector2D();
            Vector2D max = new Vector2D();

            min.x = Math.Min(this.mouseDownPos.x, this.mouseCurrentPos.x);
            min.y = Math.Min(this.mouseDownPos.y, this.mouseCurrentPos.y);
            max.x = Math.Max(this.mouseDownPos.x, this.mouseCurrentPos.x);
            max.y = Math.Max(this.mouseDownPos.y, this.mouseCurrentPos.y);

            Size2D size = new Size2D();

            size.Width = max.x - min.x;
            size.Height = max.y - min.y;

            ICanvasRenderer renderer = renderData.Renderer;

            renderer.DrawLineRect(min, size, this.settings.BoxSelectionDrawColor, SelectionRectWidth);
        }

        #endregion

        #region Methods - Protected - Input

        /// <summary>
        /// Handle left mouse button down.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected override InputHandled HandleLeftMouseButtonDown(SceneToolInputData sceneInputData, MouseButtonInputData inputData)
        {
            this.mouseDown = true;
            this.mouseDownPos = RenderUtility.CanvasPointToWorld(sceneInputData.Camera, inputData.Pos);
            this.mouseCurrentPos = this.mouseDownPos;

            return InputHandled.Yes;
        }

        /// <summary>
        /// Handle left mouse button up.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected override InputHandled HandleLeftMouseButtonUp(SceneToolInputData sceneInputData, MouseButtonInputData inputData)
        {
            // If we didn't get a mouse down, force a single select.
            // 
            // This was added so that selection cycling would work when
            // in conjunction with other tools that ate the mouse down but
            // didn't eat the mouse up.
            if (!this.mouseDown)
            {
                this.mouseDownPos = RenderUtility.CanvasPointToWorld(sceneInputData.Camera, inputData.Pos);
                this.mouseCurrentPos = this.mouseDownPos;
            }

            this.HandleSelection(sceneInputData);

            this.mouseDown = false;

            return InputHandled.Yes;
        }

        /// <summary>
        /// Handle mouse move input data.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected override InputHandled HandleMouseMove(SceneToolInputData sceneInputData, MouseMoveInputData inputData)
        {
            if (!this.mouseDown)
            {
                return InputHandled.Yes;
            }

            this.mouseCurrentPos = RenderUtility.CanvasPointToWorld(sceneInputData.Camera, inputData.Pos);

            return InputHandled.Yes;
        }

        /// <summary>
        /// Handle lost focus input data.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="inputData">Input data to handle.</param>
        /// <returns>Returns if the input data was handled or not.</returns>
        protected override InputHandled HandleLostFocus(SceneToolInputData sceneInputData, FocusInputData inputData)
        {
            this.mouseDown = false;
            return InputHandled.No;
        }

        #endregion

        #region Methods - Protected
        
        /// <summary>
        /// Called when the mouse is released to handle a selection from a click or a click and drag.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        protected virtual void HandleSelection(SceneToolInputData sceneInputData)
        {
            if (this.HasValidBoxSelectionRect())
            {
                Vector2D min = new Vector2D();
                Vector2D max = new Vector2D();

                min.x = Math.Min(this.mouseDownPos.x, this.mouseCurrentPos.x);
                min.y = Math.Min(this.mouseDownPos.y, this.mouseCurrentPos.y);
                max.x = Math.Max(this.mouseDownPos.x, this.mouseCurrentPos.x);
                max.y = Math.Max(this.mouseDownPos.y, this.mouseCurrentPos.y);

                Rect2D selRect = new Rect2D();

                selRect.Pos = min;
                selRect.Width = max.x - min.x;
                selRect.Height = max.y - min.y;

                this.HandleBoxSelection(sceneInputData, selRect);
            }
            else
            {
                this.HandlePointSelection(sceneInputData, this.mouseCurrentPos);
            }
        }

        /// <summary>
        /// Handle rectangle selection.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="rect">Rectnalge to select object in.</param>
        protected virtual void HandleBoxSelection(SceneToolInputData sceneInputData, Rect2D rect)
        {
            ISceneManager scene = Scene;
            RectCollisionRequest req = new RectCollisionRequest();

            req.Rect = rect;
            req.Camera = sceneInputData.Camera;

            this.HandleSelectionResult(sceneInputData, scene.HandleCollision(req));
        }

        /// <summary>
        /// Handle point selection.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="testPos">Point to test with.</param>
        protected virtual void HandlePointSelection(SceneToolInputData sceneInputData, Vector2D testPos)
        {
            ISceneManager scene = Scene;
            PointCollisionRequest req = new PointCollisionRequest();

            req.Point = testPos;
            req.Camera = sceneInputData.Camera;

            CollisionResult res = scene.HandleCollision(req);

            this.SelectionCycle_Handle(res);

            this.HandleSelectionResult(sceneInputData, res);
        }

        /// <summary>
        /// Handle selection collision results and build a new selection from them.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="res">Collision result. Always non-null.</param>
        protected virtual void HandleSelectionResult(SceneToolInputData sceneInputData, CollisionResult res)
        {
            EntityCollection newSelection = this.BuildSelection(sceneInputData.InputData, res.HitEntities);
            SetSceneSelectionAction action = new SetSceneSelectionAction(Scene, newSelection);

            action.Do();

            this.SendMessage(new ActionMessage(action));
        }

        /// <summary>
        /// Build a new selection from the entities that we hit and our input data.
        /// </summary>
        /// <param name="inputData">Input data.</param>
        /// <param name="hitEntities">Collection of entities that we hit.</param>
        /// <returns>Returns a collection of entities that we should select.</returns>
        protected EntityCollection BuildSelection(InputData inputData, EntityCollection hitEntities)
        {
            int modiferSetCount = 0;
            
            modiferSetCount += inputData.Alt ? 1 : 0;
            modiferSetCount += inputData.Shift ? 1 : 0;
            modiferSetCount += inputData.Control ? 1 : 0;

            // We only allow a single modifier to be pressed at a time. If we have more than
            // one we treat the selection as a normal selection and select what was hit.
            if (modiferSetCount != 1)
            {
                return hitEntities;
            }

            // We start out with the current scene selection and either add or
            // remove items from it.
            EntityCollection res = new EntityCollection(Scene.Selection);

            foreach (IEntity entity in hitEntities)
            {
                // Controls toggles selection.
                // Shift adds to selection.
                // Alt removes from selection.
                if (inputData.Control)
                {
                    if (entity.Selected)
                    {
                        res.Remove(entity);
                    }
                    else
                    {
                        res.Add(entity);
                    }
                }
                else if (inputData.Shift)
                {
                    res.Add(entity);
                }
                else if (inputData.Alt)
                {
                    res.Remove(entity);
                }
            }

            return res;
        }

        /// <summary>
        /// Tests to see if we have a valid box selection rect.
        /// </summary>
        /// <returns>Returns true if the current rect is valid.</returns>
        protected bool HasValidBoxSelectionRect()
        {
            if (!this.mouseDown)
            {
                return false;
            }

            float minBoxSize = this.settings.MinBoxSize;
            Vector2D diff = this.mouseCurrentPos - this.mouseDownPos;

            diff.x = Math.Abs(diff.x);
            diff.y = Math.Abs(diff.y);

            if (diff.x < minBoxSize || diff.y < minBoxSize)
            {
                return false;
            }

            return true;
        }

        #endregion

        #region Methods - Private

        /// <summary>
        /// Handle selection cycling given the provided collision results.
        /// </summary>
        /// <param name="res">Collision results.</param>
        private void SelectionCycle_Handle(CollisionResult res)
        {
            IEntityCollection hitEntities = res.HitEntities;

            if (!this.SelectionCycle_CanCycle(hitEntities))
            {
                this.SelectionCycle_MakeSingleSelection(res);
                this.SelectionCycle_ResetLastClickTime();
                return;
            }

            this.SelectionCycle_ResetLastClickTime();

            ISceneManager scene = this.Scene;
            IEntityCollection selection = scene.Selection;

            // SelectionCycle_CanCycle() should check this condition
            Debug.Assert(selection.Count == 1, "Current selection count must be 1.");

            IEntity selectedObject = selection[0];
            int index = hitEntities.IndexOf(selectedObject);

            // If we can't find the currently selected entity in the set of 
            // entities that we just hit, we can't cycle.
            if (index == -1)
            {
                this.SelectionCycle_MakeSingleSelection(res);
                this.SelectionCycle_ResetLastClickTime();
                return;
            }

            index = index + 1;
            if (index >= hitEntities.Count)
            {
                index = 0;
            }

            IEntity newSelection = hitEntities[index];

            hitEntities.Clear();
            hitEntities.Add(newSelection);
        }

        /// <summary>
        /// Tests to see if we can enable click selection cycling.
        /// </summary>
        /// <param name="hitEntities">Entities that we just hit with our click.</param>
        /// <returns>Returns true if we can enable selection cycling.</returns>
        private bool SelectionCycle_CanCycle(IEntityCollection hitEntities)
        {
            if (hitEntities.Count == 1)
            {
                return false;
            }

            // If we currently have multiple objects selected, we don't
            // selection cycle.
            ISceneManager scene = this.Scene;
            IEntityCollection selection = scene.Selection;
            if (selection.Count != 1)
            {
                return false;
            }

            if (!this.lastClickTime.HasValue)
            {
                return false;
            }

            TimeSpan span = DateTime.Now - this.lastClickTime.Value;
            if (span.TotalMilliseconds > (double)SelectionCycleTimeMS)
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Make the hit entities array in the passed collision result into a 
        /// single selection.
        /// </summary>
        /// <param name="res">Collision result to make into a single selection.</param>
        private void SelectionCycle_MakeSingleSelection(CollisionResult res)
        {
            if (res.HitEntities.Count <= 1)
            {
                return;
            }
             
            IEntity first = res.HitEntities[0];

            res.HitEntities.Clear();
            res.HitEntities.Add(first);
        }

        /// <summary>
        /// Reset the last click time to be the current time.
        /// </summary>
        private void SelectionCycle_ResetLastClickTime()
        {
            this.lastClickTime = DateTime.Now;
        }

        #endregion
    }
}
